// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME	   : FI.Net Application Framework 
// COMPONENT ID	   : FI.Transactions.Transaction
// COMPONENT DESC  : Transaction class, wrap the database transaction and provide the similar interface with system transaction.
//
// CREATED DATE/BY : 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY:	DATE/BY						SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
//						18 Aug 2018 / Tian Mi							Fix SOAP serilization doesn't support generic issue.
//						18 Aug 2018 / Tian Mi								Remove the commitable transaction, wrap DbTransaction.
// ==================================================================================================

using FI.Core.ApplicationContexts;
using FI.Core.Data;
using FI.Core.Properties;
using FI.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Runtime.Remoting.Messaging;

namespace FI.Core.Transactions
{
    /// <summary>
    /// Transaction class, designed to wrap database transaction.
    /// </summary>
    [Serializable]
    public class Transaction : IDisposable
    {
        #region Private variable
        private bool _isDisposed = false;
        private const string TransactionListContextKey = "FI.Transactions.KeyofTransactionList";
        private readonly static object sycHelper = new object();        

        #endregion

        /// <summary>
        /// Gets or sets the name of the database.
        /// </summary>
        /// <value>The name of the database.</value>
        public string DatabaseName { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is complete.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is complete; otherwise, <c>false</c>.
        /// </value>
        public bool IsComplete { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is inner transaction.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is inner transaction; otherwise, <c>false</c>.
        /// </value>
        public bool IsInnerTransaction { get; private set; }

        /// <summary>
        /// Retrieve inner DbTransaction instance.
        /// </summary>
        public DbTransaction DbTransaction { get; private set; }

        #region Constructor & Destructor method.

        /// <summary>
        /// Begin a transaction in default database with readcommitted isolation level.
        /// </summary>
        public Transaction()
            : this(DatabaseFactories.GetDatabase().DatabaseName)
        { }

        /// <summary>
        /// Begin a transaction in specified database with readcommitted isolation level.
        /// </summary>
        /// <param name="databaseName">Database name</param>
        /// <exception cref="ArgumentNullException">Input databaseName is null or empty.</exception>
        public Transaction(string databaseName)
                : this(databaseName, IsolationLevel.ReadCommitted)
        {}

        /// <summary>
        /// Begin a transaction in specified database with specified isolation level.
        /// </summary>
        /// <param name="databaseName">Database name</param>
        /// <param name="isolationLevel">Isolation level</param>
        /// <exception cref="ArgumentNullException">Input databaseName is null or empty.</exception>
        public Transaction(string databaseName, IsolationLevel isolationLevel)
        {
            Guard.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            this.DatabaseName = databaseName;
            Dictionary<string, DbTransaction> transactionList = GetTransactions();

            DbTransaction ambientTransaction;
            if (transactionList.TryGetValue(this.DatabaseName, out ambientTransaction))
            {
                this.DbTransaction = ambientTransaction;
                this.IsInnerTransaction = true;
            }
            else
            {
                DbConnection dbConnection = DatabaseFactories.GetDatabase(databaseName).CreateConnection();
                dbConnection.Open();
                this.DbTransaction = dbConnection.BeginTransaction(isolationLevel);
                transactionList.Add(this.DatabaseName, this.DbTransaction);
                ApplicationContext.Current.TransactionId = Guid.NewGuid().ToString();
            }
        }

        /// <summary>
        /// Disposes current transaction.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                if (disposing)
                {
                    _isDisposed = true;
                    
                    if (!this.IsInnerTransaction)
                    {
                        Dictionary<string, DbTransaction> transactionList = GetTransactions();
                        if (null != transactionList)
                        {
                            transactionList.Remove(this.DatabaseName);
                            if (transactionList.Count == 0)
                            {
                                CallContext.FreeNamedDataSlot(TransactionListContextKey);
                                ApplicationContext.Current.TransactionId = String.Empty;
                            }
                        }

                        if (null != this.DbTransaction)
                        {
                            DbConnection connection = this.DbTransaction.Connection;
                            if (null != connection)
                            {
                                if (this.IsComplete)
                                {
                                    this.DbTransaction.Commit();
                                }
                                else
                                {
                                    this.DbTransaction.Rollback();
                                }
                                connection.Close();
                            }
                        }
                    }
                }
            }
        }

        #endregion

        /// <summary>
        /// Mark the transaction as it is ready to be committed.
        /// </summary>
        /// <example>
        /// <code>
        /// using(Transaction t = new Transaction())
        /// {
        ///		/*Write your code here.*/
        ///		t.Complete();
        ///		/*Do not write any code with database access here.*/
        /// }
        /// </code>
        /// </example>
        /// <exception cref="InvalidOperationException">If this method be called more than one time, will throw an InvalidOperationException.</exception>
        public void Complete()
        {
            if (!this.IsInnerTransaction)
            {
                if (this.IsComplete)
                {
                    throw new InvalidOperationException(Resources.TransactionAlreadyCompleted);
                }
                else
                {
                    this.IsComplete = true;
                }
            }
        }

        /// <summary>
        /// Gets DbTransaction instance from application context with specified database name
        /// </summary>
        /// <param name="databaseName">The database which need to get the DbTransaction</param>
        /// <returns>return null (Nothing in VB) if no transaction in specified database, otherwise return a DbTransaction instance.</returns>
        /// <exception cref="ArgumentNullException">Input databaseName is null or empty.</exception>
       public static DbTransaction GetDbTransaction(string databaseName)
        {
            Guard.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            Dictionary<string, DbTransaction> transactionList;
            DbTransaction dbTransaction;
            transactionList = GetTransactions();
            transactionList.TryGetValue(databaseName, out dbTransaction);
            return dbTransaction;
        }

        internal static Dictionary<string, DbTransaction> GetTransactions()
        {
            if (null == CallContext.GetData(TransactionListContextKey))
            {
                lock (sycHelper)
                {
                    if (null == CallContext.GetData(TransactionListContextKey))
                    {
                        CallContext.SetData(TransactionListContextKey, new Dictionary<string, DbTransaction>());
                    }
                }
            }
            return CallContext.GetData(TransactionListContextKey) as Dictionary<string, DbTransaction>;
        }
    }
}